package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"gitee.com/fenquen/go_aop/utils"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"io"
	"log"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"
)

const empty = ""
const dot = "."

var moduleName = empty
var rootDirPackageName = empty

var config = &Config{}

var waitGroup = sync.WaitGroup{}

var mutexes = []*sync.Mutex{{}, {}, {}}

var rootDirPath string

func main() {
	readConfigFromFile()

	rootDirPath = os.Args[1]

	// rootDir对应的目录因该是1个全量的go项目 读取对应目录的go.mod
	moduleName = getModuleName(path.Join(rootDirPath, "go.mod"))

	generateDirPath := path.Join(rootDirPath, "generate")
	_ = os.RemoveAll(generateDirPath)

	change(rootDirPath)

	waitGroup.Wait()
}

func change(currentDirPath string) {
	dirEntries, err := os.ReadDir(currentDirPath)
	if err != nil {
		panic(err)
	}

	for _, dirEntry := range dirEntries {
		noNeedHandle := false

		// 如果是目录的话下钻递归
		if dirEntry.IsDir() {
			change(path.Join(currentDirPath, dirEntry.Name()))
			continue
		}

		// 如不是go文件不去handle
		if !strings.HasSuffix(dirEntry.Name(), ".go") {
			noNeedHandle = true
		}

		// test不要
		if strings.Contains(dirEntry.Name(), "_test.go") {
			noNeedHandle = true
		}

		// 要是用不了处理的话 把它copy到对应的generate体系目录
		if noNeedHandle {
			originalFile, err := os.Open(path.Join(currentDirPath, dirEntry.Name()))
			if err != nil {
				panic(err)

			}

			generatedFileParentDirPath := generateFileParentDirPath(currentDirPath)
			generatedFile, err := os.OpenFile(path.Join(generatedFileParentDirPath, dirEntry.Name()), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666)
			if err != nil {
				panic(err)
			}

			_, err = io.Copy(generatedFile, originalFile)
			if err != nil {
				panic(err)
			}

			continue
		}

		go func(dirEntry os.DirEntry) {
			waitGroup.Add(1)

			defer waitGroup.Done()

			originalGoFileName := dirEntry.Name()
			originalGoFilePath := path.Join(currentDirPath, originalGoFileName)

			fileSet := token.NewFileSet()

			astFile, err := parser.ParseFile(fileSet, originalGoFilePath, nil, parser.ParseComments)
			if err != nil {
				panic(err)
			}

			// 如何知道项目的根目录的对应的package名字是moduleName还是main 需要读取根目录下的随便1个go文件 然后读取它的package
			// 说明当前是根目录 且 尚未得到rootDirPackageName
			var currentPackagePath string
			if currentDirPath == rootDirPath && rootDirPackageName == empty {
				rootDirPackageName = astFile.Name.Name
				currentPackagePath = rootDirPackageName
			} else {
				currentPackagePath = path.Join(moduleName, strings.Replace(currentDirPath, rootDirPath, empty, -1))
			}

			proxyFuncDecls := make([]ast.Decl, 0)

			// 会用到的aspect全部的import
			aspectPackageNamesNeedImport := make(map[string]string)

			// slice是nil也能去遍历
			// 不太确定当遍历Decls时候再向其中添加元素会不会有问题 使用视图来应对 其实是不会的
			// https://uphie.studio/posts/Go%E4%B8%AD%E5%AF%B9%E4%B8%80%E4%B8%AAslice%E4%B8%80%E8%BE%B9%E9%81%8D%E5%8E%86%E4%B8%80%E8%BE%B9append%E8%83%BD%E5%90%A6%E6%97%A0%E9%99%90%E5%BE%AA%E7%8E%AF/
			for _, decl := range astFile.Decls {
				switch decl0 := decl.(type) {
				case *ast.FuncDecl:
					originalFuncDecl := decl0

					// 函数是不是有receiver
					hasReceiver := originalFuncDecl.Recv != nil
					hasReceiverName := false
					if hasReceiver {
						hasReceiverName = utils.SliceIsNotEmpty(originalFuncDecl.Recv.List[0].Names)
					}
					// 如果有receiver的话还要确定是不是书写了参数名 例如 func(*A)(){} 虽然有receiver然而却是少了名
					// 要未写的话要在生成的函数里生成1个 后续调用原函数的时候要用
					receiverName := empty
					if hasReceiver {
						// 说明receiver是有名字的
						if hasReceiverName {
							receiverName = originalFuncDecl.Recv.List[0].Names[0].Name
						} else { // 说明尚未给receiver命名未用到它,生成1个给生成的proxy函数使用来调用原函数
							receiverName = "generatedReceiver"
						}
					}

					// 函数原名
					originalFuncName := originalFuncDecl.Name.Name

					// 函数的path
					functionPath := currentPackagePath + "." + originalFuncName

					// 如果是main.main() 那么需要跳多
					if functionPath == "main.main" {
						continue
					}

					// 如果是init() 需要跳过
					if originalFuncName == "init" {
						continue
					}

					// 如果是aspect相应的函数的话 需要跳过
					switch functionPath {
					case config.Before.AspectPath:
						continue
					case config.Around.AspectPath:
						continue
					case config.After.AspectPath:
						continue
					}

					// 原来的函数名字由A变为a_original
					originalFuncNameNew := utils.Lower1stChar(originalFuncName) + "_original"
					originalFuncDecl.Name.Name = originalFuncNameNew

					// 函数是不是可变参数的 原函数的参数的ident
					isVariadic := false
					variadicParamName := empty
					var originalFuncParamsIndent []ast.Expr
					for _, param := range originalFuncDecl.Type.Params.List {
						if _, variadic := param.Type.(*ast.Ellipsis); variadic {
							isVariadic = true
							variadicParamName = param.Names[0].Name
						}

						originalFuncParamsIndent = append(originalFuncParamsIndent, ast.NewIdent(param.Names[0].Name))
					}

					// 应对returnedValue
					hasReturnedValues := originalFuncDecl.Type.Results != nil
					hasNamedReturnedValues := false
					// 不管函数的返回的值是不是命名的统1应对
					var returnedValueNames []string
					var returnedValueNamesIdent []ast.Expr
					var returnedValueTypes []ast.Expr
					if hasReturnedValues {
						for a, returnedField := range originalFuncDecl.Type.Results.List {
							var returnedValueName string

							// 说明函数的返回的值是命名的
							if returnedField.Names != nil {
								hasNamedReturnedValues = true
								returnedValueName = returnedField.Names[0].Name
							} else {
								returnedValueName = "returnedValue" + strconv.Itoa(a)
							}

							returnedValueNames = append(returnedValueNames, returnedValueName)
							returnedValueNamesIdent = append(returnedValueNamesIdent, ast.NewIdent(returnedValueName))
							returnedValueTypes = append(returnedValueTypes, returnedField.Type)
						}
					}

					// 生成了proxy函数的FuncDecl
					proxyFuncDecl := &ast.FuncDecl{
						Recv: originalFuncDecl.Recv,
						Name: ast.NewIdent(originalFuncName),
						Type: originalFuncDecl.Type,
						Body: &ast.BlockStmt{},
					}

					// 要是有receiver 兜底的补上receiverName
					if hasReceiver {
						if hasReceiverName {
							proxyFuncDecl.Recv.List[0].Names[0].Name = receiverName
						} else {
							proxyFuncDecl.Recv.List[0].Names = []*ast.Ident{{Name: receiverName}}
						}
					}

					proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, &ast.ExprStmt{
						//X: &ast.BasicLit{Kind: token.STRING, Value: ""},
						X: ast.NewIdent("\n"),
					})

					// 调用before
					for _, pattern := range config.Before.PointcutExprs {
						// 说明需要调用before
						if satisfyPointcutExpr(functionPath, pattern) {
							packageName, funcName := extractAspectPath(config.Before.AspectPath)

							aspectPackageNamesNeedImport[strings.Split(config.Before.AspectPath, dot)[0]] = empty

							callBeforeExpr := &ast.CallExpr{
								Fun: &ast.SelectorExpr{
									X:   ast.NewIdent(packageName),
									Sel: ast.NewIdent(funcName),
								},
								Args: []ast.Expr{ast.NewIdent(strconv.FormatBool(isVariadic))},
							}
							callBeforeExpr.Args = append(callBeforeExpr.Args, originalFuncParamsIndent...)

							callBeforeExprStmt := &ast.ExprStmt{X: callBeforeExpr}
							proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, callBeforeExprStmt)

							break
						}
					}

					// 调用原来的函数还是around 需要用到receiverName
					callOrigin := true
					for _, pattern := range config.Around.PointcutExprs {
						if satisfyPointcutExpr(functionPath, pattern) {
							callOrigin = false
							break
						}
					}
					if callOrigin {
						var callOriginalExpr *ast.CallExpr
						if hasReceiver { // receiverName.函数名字()
							callOriginalExpr = &ast.CallExpr{
								Fun: &ast.SelectorExpr{
									X:   ast.NewIdent(receiverName),
									Sel: ast.NewIdent(originalFuncNameNew),
								},
								Args: originalFuncParamsIndent,
							}
						} else {
							callOriginalExpr = &ast.CallExpr{
								Fun:  ast.NewIdent(originalFuncNameNew),
								Args: originalFuncParamsIndent,
							}
						}

						// 函数如果是可变参数的话 调用original时候需要化整为零
						if isVariadic {
							argCount := len(callOriginalExpr.Args)
							callOriginalExpr.Args = append(callOriginalExpr.Args[0:argCount-1], ast.NewIdent(variadicParamName+"..."))
						}

						if hasReturnedValues {
							tok := token.DEFINE
							if hasNamedReturnedValues {
								tok = token.ASSIGN
							}

							// rootDirPackageName:=原来的函数调用()
							assignStmt := &ast.AssignStmt{
								Lhs: returnedValueNamesIdent,
								Tok: tok,
								Rhs: []ast.Expr{callOriginalExpr},
							}
							proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, assignStmt)
						} else { // 原来的函数调用()
							callOriginalStmt := &ast.ExprStmt{X: callOriginalExpr}
							proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, callOriginalStmt)
						}
					} else { // 调用around需要用到receiverName
						var function ast.Expr

						if hasReceiver {
							function = &ast.SelectorExpr{
								X:   ast.NewIdent(receiverName),
								Sel: ast.NewIdent(originalFuncNameNew),
							}
						} else {
							function = ast.NewIdent(originalFuncNameNew)
						}

						packageName, funcName := extractAspectPath(config.Around.AspectPath)

						aspectPackageNamesNeedImport[strings.Split(config.Around.AspectPath, dot)[0]] = empty

						callAroundExpr := &ast.CallExpr{
							Fun: &ast.SelectorExpr{
								X:   ast.NewIdent(packageName),
								Sel: ast.NewIdent(funcName),
							},
							Args: []ast.Expr{function, ast.NewIdent(strconv.FormatBool(isVariadic))},
						}
						callAroundExpr.Args = append(callAroundExpr.Args, originalFuncParamsIndent...)

						// 要是函数有返回值的话需要收取
						if hasReturnedValues {
							// returnedValues:=processor.Around()
							assignStmt := &ast.AssignStmt{
								Lhs: []ast.Expr{ast.NewIdent("returnedValues")},
								Tok: token.DEFINE,
								Rhs: []ast.Expr{callAroundExpr},
							}
							proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, assignStmt)

							tok := token.DEFINE
							if hasNamedReturnedValues {
								tok = token.ASSIGN
							}

							// returnedValue0,_:=returnedValues[0].(string)
							for a, returnedValueNameIdent := range returnedValueNamesIdent {
								assignStmt := &ast.AssignStmt{
									Lhs: []ast.Expr{returnedValueNameIdent, ast.NewIdent("_")},
									Tok: tok,
									Rhs: []ast.Expr{
										&ast.TypeAssertExpr{
											X: &ast.IndexExpr{
												X:     ast.NewIdent("returnedValues"),
												Index: ast.NewIdent(strconv.Itoa(a)),
											},
											Type: returnedValueTypes[a],
										},
									},
								}

								appendToFuncDeclTail(proxyFuncDecl, assignStmt)
							}
						} else { // processor.Around()
							callAroundStmt := &ast.ExprStmt{X: callAroundExpr}
							proxyFuncDecl.Body.List = append(proxyFuncDecl.Body.List, callAroundStmt)
						}
					}

					// 调用after
					callAfter := false
					for _, pattern := range config.After.PointcutExprs {
						if satisfyPointcutExpr(functionPath, pattern) {
							callAfter = true
						}
					}
					if callAfter {
						packageName, funcName := extractAspectPath(config.After.AspectPath)

						aspectPackageNamesNeedImport[strings.Split(config.After.AspectPath, dot)[0]] = empty

						afterFunction := &ast.SelectorExpr{
							X:   ast.NewIdent(packageName),
							Sel: ast.NewIdent(funcName),
						}

						if hasReturnedValues {
							// processor.After(returnedValueNamesIdent[0],returnedValueNamesIdent[1])
							// processor.After(returnedValue0)
							callAfterExpr := &ast.CallExpr{
								Fun:  afterFunction,
								Args: returnedValueNamesIdent,
							}

							appendToFuncDeclTail(proxyFuncDecl, &ast.ExprStmt{X: callAfterExpr})
						} else {
							// processor.After()
							callAfterExpr := &ast.CallExpr{Fun: afterFunction}
							appendToFuncDeclTail(proxyFuncDecl, &ast.ExprStmt{X: callAfterExpr})
						}
					}

					// 应对return
					if hasReturnedValues {
						returnStmt := &ast.ReturnStmt{}
						// 如果函数的返回的值是有名字的 那么直接return
						if hasNamedReturnedValues {

						} else {
							returnStmt.Results = returnedValueNamesIdent
						}

						appendToFuncDeclTail(proxyFuncDecl, returnStmt)
					}

					// proxy函数已构建完毕 添加到astFile尾部
					proxyFuncDecls = append(proxyFuncDecls, proxyFuncDecl)
				}
			}

			// 得到当前的目录对应的generate体系的目录
			generatedGoFileParentDirPath := generateFileParentDirPath(currentDirPath)

			generatedGoFilePath := path.Join(generatedGoFileParentDirPath, originalGoFileName)

			generatedGoFile, err := os.OpenFile(generatedGoFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666)
			if err != nil {
				panic(err)
			}

			err = printer.Fprint(generatedGoFile, fileSet, astFile)
			if err != nil {
				panic(err)
			}

			a := &ast.BasicLit{Kind: token.STRING, Value: "\n// -----------------below generated-----------------\n\n"}
			_ = printer.Fprint(generatedGoFile, fileSet, a)

			for _, proxyFuncDecl := range proxyFuncDecls {
				_ = printer.Fprint(generatedGoFile, fileSet, proxyFuncDecl)
				_ = printer.Fprint(generatedGoFile, fileSet, &ast.BasicLit{Kind: token.STRING, Value: "\n\n"})
			}

			_ = generatedGoFile.Close()

			handleGeneratedGoFile(generatedGoFilePath, aspectPackageNamesNeedImport)
		}(dirEntry)
	}
}

// 生成的go文件的起始的地点(package之前)添加 // generated file do not edit
// 要是原来的文件打头的地点有了comment 那么很容易处理 只需要把内容加到相应的commentGroup便可以了
// 要是原来不存在的话就有点不容易了 确实是可以 强行生成1个的commentGroup填充内容然后把commentGroup设置到ast.Doc和ast.Comment
// 然而生成的go文件会是这样的
//
// package // generated file do not edit
// test
//
// 是不够漂亮的
// 干脆使用原始的暴力的套路使用普通的读写文件 新生成1个文件 把内容添加到该文件的打头 然后写入原go文件的全部内容
// 生成的文件是 rootDirPackageName.go.temp
// 使用原始的文件读写的套路实现对文件的打头的部分写入 // file generated by go_aop do not edit 和 import
func handleGeneratedGoFile(generatedGoFilePath string, aspectPackageNamesNeedImport map[string]string) {
	originalGoFile, _ := os.Open(generatedGoFilePath)

	goTempFilePath := generatedGoFilePath + ".temp"

	goTempFile, err := os.OpenFile(goTempFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		panic(err)
	}

	// 头部写入 // generated file do not edit
	_, _ = goTempFile.WriteString("// file generated by go_aop do not edit\n")

	buffer := make([]byte, 1)
	beforePackageClauseContent := make([]byte, 0)
	// 用来读取"package " 不忘了后边的空白
	packageClauseContent := make([]byte, 7)

a:
	for {
		_, _ = originalGoFile.Read(buffer)

		if 'p' == buffer[0] {
			position, _ := originalGoFile.Seek(0, io.SeekCurrent)

			for {
				readNum, _ := originalGoFile.Read(packageClauseContent)
				if "ackage " == string(packageClauseContent[:readNum]) {
					packageClauseContent = append([]byte{'p'}, packageClauseContent...)

					for {
						_, _ = originalGoFile.Read(buffer)

						packageClauseContent = append(packageClauseContent, buffer[0])

						switch buffer[0] {
						case byte('\n'):
							fallthrough
						case byte(';'): // go代码都写在1行的话使用;
							break a
						}
					}
				} else {
					_, _ = originalGoFile.Seek(position, io.SeekStart)
					beforePackageClauseContent = append(beforePackageClauseContent, buffer[0])
					break
				}
			}
		} else {
			beforePackageClauseContent = append(beforePackageClauseContent, buffer[0])
		}
	}

	// 把原文件的package clause前边的内容 写到新的文件
	_, _ = goTempFile.Write(beforePackageClauseContent)

	// 把原文件的package clause 写到新的文件
	_, _ = goTempFile.Write(packageClauseContent)

	// 写入需要用到的import
	// 要是import都是相同的需要去重
	for importPath := range aspectPackageNamesNeedImport {
		_, _ = goTempFile.WriteString(fmt.Sprintf("import \"%s\"\n", importPath))
	}

	// 写入原go文件的剩下内容
	buffer = make([]byte, 4096)
	for {
		readNum, err := originalGoFile.Read(buffer)
		if err == io.EOF {
			break
		}

		if err != nil {
			panic(err)
		}

		_, _ = goTempFile.Write(buffer[:readNum])
	}

	_ = goTempFile.Close()

	_ = originalGoFile.Close()

	//os.Remove(generatedGoFilePath)
	err = os.Rename(goTempFilePath, generatedGoFilePath)
	if err != nil {
		panic(err)
	}

}

func appendToFuncDeclTail(funcDecl *ast.FuncDecl, stmt ast.Stmt) {
	funcDecl.Body.List = append(funcDecl.Body.List, stmt)
}

type Config struct {
	Before struct {
		PointcutExprs []string `json:"pointcutExprs"`
		AspectPath    string   `json:"aspectPath"`
	} `json:"before"`
	Around struct {
		PointcutExprs []string `json:"pointcutExprs"`
		AspectPath    string   `json:"aspectPath"`
	} `json:"around"`
	After struct {
		PointcutExprs []string `json:"pointcutExprs"`
		AspectPath    string   `json:"aspectPath"`
	} `json:"after"`
}

func readConfigFromFile() {
	configFilePath := os.Args[2]
	flag.Parse()

	if configFilePath == empty {
		log.Fatal("you have not designate readme.md config file path")
	}

	configFile, err := os.Open(configFilePath)
	if err != nil {
		log.Fatalf("open config file failed: %s \n", err.Error())
	}

	byteArr, err := io.ReadAll(configFile)
	if err != nil {
		log.Fatalf("read config file failed: %s \n", err.Error())
	}

	err = json.Unmarshal(byteArr, config)
	if err != nil {
		log.Fatalf("unmarshal failed: %s \n", err.Error())
	}

}

// functionPath server/test.show
// pointcutExpr server/test/*
func satisfyPointcutExpr(functionPath string, pointcutExpr string) bool {
	functionPathLen := len(functionPath)
	pointcutExprLen := len(pointcutExpr)

	minLen := functionPathLen
	if functionPathLen > pointcutExprLen {
		minLen = pointcutExprLen
	}

	functionPathTail := empty
	pointcutExprTail := empty
	for a := 0; a < minLen; a++ {
		if functionPath[a] != pointcutExpr[a] {
			functionPathTail = functionPath[a:]
			pointcutExprTail = pointcutExpr[a:]
			break
		}
	}

	if functionPathTail == pointcutExprTail {
		return true
	}

	if pointcutExprTail == "*" {
		return true
	}

	if functionPathTail == strings.Replace(pointcutExprTail, "*", empty, -1) {
		return true
	}

	return false
}

func getModuleName(goModFilePath string) string {
	goModFile, err := os.Open(goModFilePath)
	if err != nil {
		panic(err)
	}

	bufioReader := bufio.NewReader(goModFile)

	moduleLine, err := bufioReader.ReadString(byte('\n'))
	if err != nil {
		panic(err)
	}

	return strings.Replace(moduleLine, "module ", empty, 1)
}

func extractAspectPath(aspectPath string) (string, string) {
	elementSlice := strings.Split(aspectPath, dot)

	lastIndex := strings.LastIndex(elementSlice[0], "/")
	packageName := elementSlice[0][lastIndex+1:]

	funcName := elementSlice[1]

	return packageName, funcName
}

func generateFileParentDirPath(currentDirPath string) string {
	packagePath := strings.Replace(currentDirPath, rootDirPath, empty, -1)

	generatedFileParentDirPath := path.Join(rootDirPath, "generate", packagePath)

	mutexIndex := len(generatedFileParentDirPath) % len(mutexes)

	mutexes[mutexIndex].Lock()

	err := os.MkdirAll(generatedFileParentDirPath, 0750)
	if err != nil {
		panic(err)
	}

	mutexes[mutexIndex].Unlock()

	return generatedFileParentDirPath
}
